////////////////////////////////////////////////////////////////////
///
///   EmailMessage.cpp - Mime message parser implementation
///
///   Copyright (C) 2007 ScalingWeb.com
///   All rights reserved. 
/// 
///   Authors: Yuriy Soroka <ysoroka@scalingweb.com>
///	       Anton Fedoruk <afedoruk@scalingweb.com>
///
/// 
///////////////////////////////////////////////////////////////////


#include "EmailMessage.h"
#include <QtCore>
#include "StringUtils.h"
#include <stack>
#include "time.h"
#include <QTextCodec>

const char constTextPlain[]	= "text/plain";
const char constTextHtml[]	= "text/html";
const char constRfc822[]	= "message/rfc822";
const char constMultipart[]	= "multipart";
const char constBoundary[]	= "boundary";
const char constCharset[]	= "charset";

const int MaxSize = 32767;

/// Function HexCode
/// Return decimal code of a hex number
inline unsigned char HexCode( const char ch )
{
	if ( ch > 47 && ch < 58 )
	{
		return ( ch - 48 );
	}
	else
	if ( ch > 64 && ch < 71 )
	{
		return ( ch - 55 );
	}

	return 0;
}

///
///  MailHeader 
///

MailHeader::MailHeader()
{
	clear();
}

MailHeader::~MailHeader()
{}

const QString& MailHeader::getMessageId() const
{
	return messageId_;
}

const QStringList& MailHeader::getTo() const
{
	return recipients_;
}

const QString& MailHeader::getFrom() const
{
	return sender_;
}

const QStringList& MailHeader::getCc() const
{
	return cc_;
}

const QStringList& MailHeader::getBcc() const
{
	return bcc_;
}


const QString& MailHeader::getSubject() const
{
	return subject_;
}

long MailHeader::getDate() const
{
	return date_;
}

uint MailHeader::getPriority() const
{
	return priority_;
}

void MailHeader::clear()
{
	messageId_ = "";
	recipients_.clear();
	cc_.clear();
	bcc_.clear();
	sender_ = "";
	subject_ = "";
	date_ = 0;
	priority_ = 3;
}

///
///	EmailMessage helps with parsing raw bytes 
/// email messages, and extracting all necessary information
///

EmailMessage::EmailMessage()
{
	bParseHeaderOnly_ = false;
	init();
}

EmailMessage::~EmailMessage()
{		
	if ( bAutoDelete_ )
	{
		emailStream_->close();
		delete emailStream_;
	}
}

///
///	@return true if message parsed successfully, and false otherwise
///
bool EmailMessage::open(JInputStream *stream)
{
	// Initialize
	init();

	if ( 0 == stream )
	{
		return false;
	}

	stream->reset();
	emailStream_ = stream;
	readline_.setSource( emailStream_ );

	bool bUnfolding = false;

	qint64 lastReadPos = 0;

	char lineBuf[ MaxSize + 1 ];
	char tempBuf[ MaxSize + 1 ];

	JBufferString line( lineBuf, MaxSize );
	JBufferString temp( tempBuf, MaxSize );

	temp.clear();
	long lineSize = readline_.readLine( temp.buffer(), MaxSize );
	temp.setSize( lineSize );

	temp.trim();

	if ( readline_.atEnd() )
	{
		// File is empty or have empty header
		return false;
	}

	// Parsing the Header
	while ( !readline_.atEnd() )
	{
		lastReadPos = readline_.pos();

		line.clear();
		lineSize = readline_.readLine( line.buffer(), MaxSize );
		line.setSize( lineSize );

		bUnfolding = false;
		if ( line.startsWith( ' ' ) || line.startsWith( '\t' ) )
		{
			bUnfolding = true;
		}

		line.trim();

		if ( line.isEqual( boundary_.data(), boundary_.size() ) )
		{
			// Save boundary stream pointer for readAttachment fast use
			boundaryPos_ = lastReadPos;

			processParameter( temp );
			break;
		}

		// If first symbol is LWSP then it's continuation of previous string
		// otherwise parse the previous string
		if ( bUnfolding )
		{
			// Unfolding
			temp.add( line.data(), line.size() );
		}
		else
		{
			processParameter( temp );
			temp.assign( line.data(), line.size() );
		}
	}

	// If attain end of file not lose last string
	if ( readline_.atEnd() )
	{
		processParameter( temp );
	}

	if ( !bParseHeaderOnly_ )
	{
		// Parsing the body
		return parseMimeBody();
	}

	return true;
}

///
///	@return mime type of a message
///
const QByteArray& EmailMessage::contentType()
{
	return retContentType_;
}

///
///	@return message text
///
const QString& EmailMessage::text()
{
	return text_;
}

///
/// @return email message size
///

size_t EmailMessage::size() const
{
	return emailStream_->size();
}

/// Attachments

/// 
/// @return true if email has attachments, otherwise false
/// 
bool EmailMessage::hasAttachments() const 
{
	return ( bool ) attachments_.size();
}

///
///  @return list of filenames of available attachments.
///
QStringList EmailMessage::attachments()
{
	QStringList attachs;

	for ( int i = 0; i < attachments_.size(); i++ )
	{
		attachs.append( attachments_[ i ].AttachmentName );
	}

	return attachs;
}

///
///	Reads attachment specified with file name
/// @param name - Attachment file name
/// @param container to place contents of an attachment or empty byte array if 
///			no such file exists.
///
bool EmailMessage::readAttachment(const QString& name, QByteArray& contents, QByteArray& contentType)
{
	contents.clear();

	// Find name in list
	qint64 offset = 0;
	for ( int i = 0; i < attachments_.size(); i++ )
	{
		if ( attachments_[ i ].AttachmentName == name )
		{
			offset = attachments_[ i ].FilePos;
			break;
		}
	}

	if ( !offset )
	{
		// Entry not found
		return false;
	}

	readline_.seek( offset );

	if ( readline_.atEnd() ) return false;

	// Buffers
	char lineBuf[ MaxSize + 1 ];
	char tempBuf[ MaxSize + 1 ];
	char strNameBuf[ MaxSize + 1 ];
	char localBoundaryBuf[ MaxSize + 1 ];
	char boundaryBuf[ MaxSize + 1 ];
	char endBoundaryBuf[ MaxSize + 1 ];

	JBufferString line( lineBuf, MaxSize );
	JBufferString temp( tempBuf, MaxSize );
	JBufferString strName( strNameBuf, MaxSize );
	JBufferString localBoundary( localBoundaryBuf, MaxSize );
	JBufferString boundary( boundaryBuf, MaxSize );
	JBufferString endBoundary( endBoundaryBuf, MaxSize );

	// Get the boundary
	boundary.clear();
	long lineSize = readline_.readLine( boundary.buffer(), MaxSize );
	boundary.setSize( lineSize );

	// Check for UUEncoded message
	if ( boundary.startsWith( "begin " ) )
	{
		QByteArray filename;
		if ( isUUencoded( boundary, filename ) )
		{
			uudecode( true, contents );
		}
	}
	else
	{
		boundary.trim();

		endBoundary.assign( boundary.data(), boundary.size() );
		endBoundary.add( "--", 2 );

		localBoundary.assign( "", 0 );
		bool bHeader = true;		// What is now parsing Header or Body
		bool bUnfolding = false;
		int nPos = 0;

		while( !readline_.atEnd() )
		{
			line.clear();
			lineSize = readline_.readLine( line.buffer(), MaxSize );
			line.setSize( lineSize );

			// Check for the string continuation
			if ( line.startsWith( ' ' ) || line.startsWith( '\t' ) )
			{
				bUnfolding = true;
			}
			else
			{
				bUnfolding = false;
			}

			line.trim();

			if ( bHeader )
			{
				if ( bUnfolding && bHeader )
				{
					// Unfolding
					temp.add( line.data(), line.size() );
				}
				else
				{
					// Start treat parameter
					if ( -1 != ( nPos = temp.indexOf( ':' ) ) )
					{
						strName.assign( temp.data(), temp.size() );
						strName.mid( 0, nPos );
						strName.trim();
						temp.remove( 0, nPos + 1 );

						temp.trim();

						// Multipart Content-Type
						if ( strName.compareNoCase( "Content-Type" ) )
						{
							contentType = temp.data();
						}
						else if ( strName.compareNoCase( "Content-Transfer-Encoding" ) )
						{
							// Getting local Content-Transfer-Encoding
							if ( temp.startsWith( '"' ) && temp.endsWith( '"' ) )
							{
								temp.remove( 0, 1 );
								temp.remove( temp.size() - 1, 1 );
							}

							encoding_ = temp.data();
						}
					} // end treat parameter

					temp.assign( line.data(), line.size() );
				}

				// Parse Header
				if ( line.isEqual( localBoundary.data(), localBoundary.size() ) )
				{
					if ( localBoundary.isEqual( boundary.data(), boundary.size() )  )
					{
						temp.clear();
						localBoundary.clear();
						continue;
					}
					else
					{
						// Next is parsing of body
						bHeader = false;
					}
				}
				else
				{
					if ( line.isEqual( endBoundary.data(), endBoundary.size() ) ||
						line.isEqual( boundary.data(), boundary.size() ) )
					{
						break;
					} 
				}
			}
			else
			{
				// Parse Body
				if ( line.isEqual( endBoundary.data(), endBoundary.size() ) || 
					line.isEqual( boundary.data(), boundary.size() ) )
				{
					break;
				}
				else
				{
					contents.append( line.buffer() );
					contents.append( "\r\n" );
				}
			} // Parsing of body
		} // while

		temp.assign( encoding_.data(), encoding_.size() );
		encode( contents, temp );
	}

	return true;
}

///
/// Says delete or not stream in destructor

void EmailMessage::setDeleteStream( bool isDelete )
{
	bAutoDelete_ = isDelete;
}

void EmailMessage::init()
{
	initParams();
	MailHeader::clear();
	text_ = "";
	attachments_.clear();
	boundaryPos_ = 0;
	bAddAttachment_ = false;
	attachName_ = "";
	attachPos_ = 0;
	bAutoDelete_ = false;
}

void EmailMessage::initParams()
{
	boundary_ = "";
	encoding_ = "";
	contentType_ = "";
	charset_ = "us-ascii";

	enumConType_ = e_text;		// Default Content-Type is text/plain
}

void EmailMessage::processParameter( JBufferString &paramText )
{
	char nameBuf[ MaxSize + 1 ];
	JBufferString name( nameBuf, MaxSize );
	name.clear();
	int nPos = 0;

	// Divide string onto param name and value and parse param
	if ( ( nPos = paramText.indexOf( ':' ) ) != -1 )
	{
		name.assign( paramText );
		name.mid( 0, nPos );
		name.trim();

		paramText.remove( 0, nPos + 1 );
		paramText.trim();

		if ( name.compareNoCase( "Content-Type" ) )
		{
			parseContentType( paramText );
		}
		else if ( name.compareNoCase( "From" ) )
		{
			parseFrom( paramText );
		}
		else if ( name.compareNoCase( "To" ) )
		{
			parseAddrList( recipients_, paramText );
		}
		else if ( name.compareNoCase( "Message-Id" ) )
		{
			if ( paramText.startsWith( '<' ) && paramText.endsWith( '>' ) )
			{
				paramText.remove( 0, 1 );
				paramText.remove( paramText.size() - 1, 1 );
			}

			messageId_ = paramText.data();
		}
		else if ( name.compareNoCase( "Subject" ) )
		{
			subject_ = decodeNonAsciiText( paramText );
		}
		else if ( name.compareNoCase( "Content-Transfer-Encoding" ) )
		{
			encoding_ = paramText.data();
		}
		else if ( name.compareNoCase( "Cc" ) )
		{
			parseAddrList( cc_, paramText );
		}
		else if ( name.compareNoCase( "Bcc" ) )
		{
			parseAddrList( bcc_, paramText );
		}
		else if ( name.compareNoCase( "X-Priority" ) )
		{
			priority_ = atoi( paramText.data() );
		}
		else if ( name.compareNoCase( "Date" ) )
		{
			parseDate( paramText );
		}
	}
}

//	============================================================
//	EmailMessage::parseMimeBody
///
/// Treat Content of the MIME file, beforehand have read the main
/// header and sets the enumConType_ member

bool EmailMessage::parseMimeBody()
{
	// Data declarations
	QStack< QByteArray > boundaryStack;
	char lineBuf[ MaxLineSize + 1 ];
	JBufferString line( lineBuf, MaxLineSize );

	char boundaryBuf[ MaxSize + 1 ];
	JBufferString boundary( boundaryBuf, MaxSize );
	boundary.assign( boundary_.data(), boundary_.size() );

	char nameBuf[ MaxSize + 1 ];
	char tempBuf[ MaxLineSize + 1 ];
	JBufferString name( nameBuf, MaxSize );
	JBufferString temp( tempBuf, MaxLineSize );

	char localBoundaryBuf[ MaxSize + 1 ];
	JBufferString localBoundary( localBoundaryBuf, MaxSize );
	localBoundary.clear();

	char endBoundaryBuf[ MaxSize + 1 ];
	JBufferString endBoundary( endBoundaryBuf, MaxSize );

	endBoundary.assign( boundary.data(), boundary.size() );
	endBoundary.add( "--", 2 );

	QByteArray extractedText;

	char charsetBuf[ MaxSize + 1 ];
	JBufferString charset( charsetBuf, MaxSize );
	char encodingBuf[ MaxSize + 1 ];
	JBufferString encoding( encodingBuf, MaxSize );
	encoding.clear();
	charset.clear();

	charset.assign( charset_.data(), charset_.size() );
	encoding.assign( encoding_.data(), encoding_.size() );

	bool bHeader = true;		// What is now parsing Header or Body
	bool bMulti = false;
	bool Result = true;
	bool bWasText = false, bWasHtml = false;
	bool bUnfolding = false;
	int nPos = 0;
	qint64 boundaryPos = boundaryPos_;
	int nameCounter = 0;
	long lineSize = 0;

	retContentType_ = contentType_;

	if ( e_multipart == enumConType_ )
	{
		temp.clear();
		lineSize = readline_.readLine( temp.buffer(), MaxLineSize ); 
		temp.setSize( lineSize );
		temp.trim();

		if ( readline_.atEnd() )
		{
			// No end boundary
			return false;
		}
		else if ( temp.isEqual( endBoundary.data(), endBoundary.size() ) )
		{
			// End boundary just after start boundary
			return true;
		}

		while( !readline_.atEnd() )
		{
			qint64 curLinePos = readline_.pos();
			
			line.clear();
			lineSize = readline_.readLine( line.buffer(), MaxLineSize ); 
			line.setSize( lineSize );

			// Check for the string continuation
			if ( line.startsWith( ' ' ) || line.startsWith( '\t' ) )
			{
				bUnfolding = true;
			}
			else
			{
				bUnfolding = false;
			}

			line.trim();

			if ( bHeader )
			{
				if ( bUnfolding )
				{
					// Unfolding
					temp.add( line );
				}
				else
				{
					// Process parameter
					if ( -1 != ( nPos = temp.indexOf( ':' ) ) )
					{
						name.assign( temp );
						name.mid( 0, nPos );
						name.trim();
						temp.remove( 0, nPos + 1 );

						temp.trim();

						// Multipart Content-Type
						if ( name.compareNoCase( "Content-Type" ) )
						{
							initParams();

							// The check on nested multipart content-types
							parseContentType( temp );

							if ( e_multipart == enumConType_ )
							{
								bHeader = true;
								boundaryStack.push( QByteArray( boundary.data() ) );
								localBoundary.assign( boundary_.data(), boundary_.size() );
								boundary.assign( localBoundary );
								endBoundary.assign( boundary );
								endBoundary.add( "--", 2 );
							}

							// We take only first text part (or first html if text not found)
							if ( temp.startsWith( constTextPlain ) || temp.startsWith( constRfc822 ) )
							{
								if ( bWasText || bWasHtml )
								{
									enumConType_ = e_other;
								}
								else
								{
									//charset = charset_;
									retContentType_ = contentType_;
									charset.assign( charset_.data(), charset_.size() );
									bWasText = true;
									extractedText.clear();
								}
							}
							else if ( temp.startsWith( constTextHtml ) )
							{
								if ( bWasHtml )
								{
									enumConType_ = e_other;
								}
								else
								{
									retContentType_ = contentType_;
									charset.assign( charset_.data(), charset_.size() );
									bWasHtml = true;
									extractedText.clear();
								}
							}
						}
						else if ( name.compareNoCase( "Content-Disposition" ) )
						{
							parseContentDisp( temp, bMulti, extractedText.isEmpty(),
								boundaryPos, nameCounter );
						}
						else if ( /*e_text == enumConType_ &&*/ extractedText.isEmpty() &&
							name.compareNoCase( "Content-Transfer-Encoding" ) )
						{
							// Getting local Content-Transfer-Encoding
							if ( temp.startsWith( '"' ) && temp.endsWith( '"' ) )
							{
								temp.remove( 0, 1 );
								temp.remove( temp.size() - 1, 1 );
							}

							encoding.assign( temp );
						}
					} // end treat parameter

					temp.assign( line );
				}


				// Parse Header
				if ( line.isEqual( localBoundary.data(), localBoundary.size() ) )
				{
					if ( extractedText.isEmpty() && e_text == enumConType_ )
					{
						bMulti = false;
						bAddAttachment_ = false;
					}

					addAttachment();

					if ( localBoundary.isEqual( boundary.data(), boundary.size() ) )
					{
						boundaryPos = curLinePos;
						temp.clear();
						localBoundary.clear();
						continue;
					}
					else
					{
						// Next is parsing of body
						bHeader = false;
					}
				}
				else
				{
					if ( line.isEqual( endBoundary ) )
					{
						if ( extractedText.isEmpty() && e_text == enumConType_ )
						{
							bMulti = false;
							bAddAttachment_ = false;
						}

						addAttachment();

						boundaryPos = curLinePos;

						// End of part
						if ( boundaryStack.isEmpty() ) return true;

						bHeader = true;
						bMulti = false;

						// Exit from nested part
						localBoundary.assign( boundaryStack.top().data(), boundaryStack.top().size() );
						boundary.assign( localBoundary );
						endBoundary.assign( boundary );
						endBoundary.add( "--", 2 );

						boundaryStack.pop();
					}
					else if ( line.isEqual( boundary ) )
					{
						if ( extractedText.isEmpty() && e_text == enumConType_ )
						{
							bMulti = false;
							bAddAttachment_ = false;
						}

						addAttachment();

						boundaryPos = curLinePos;
					}
				}
			}
			else
			{
				// Parse Body
				if ( line.isEqual( endBoundary ) )
				{
					boundaryPos = curLinePos;
					if ( boundaryStack.isEmpty() ) 
					{
						Result = true;
						break;
					}

					bHeader = true;
					bMulti = false;

					localBoundary.assign( boundaryStack.top().data(), boundaryStack.top().size() );
					boundary.assign( localBoundary );
					endBoundary.assign( boundary );
					endBoundary.add( "--", 2 );

					boundaryStack.pop();
				}
				else if ( line.isEqual( boundary ) ) 
				{
					boundaryPos = curLinePos;
					bHeader = true;
					bMulti = false;

					temp.clear();
					lineSize = readline_.readLine( temp.buffer(), MaxLineSize ); 
					temp.setSize( lineSize );

					if ( readline_.atEnd() )
					{
						// No end boundary
						Result = false;
						break;
					}
				}
				else
				{
					if ( e_text == enumConType_ && !bMulti )	// it's may be Content-Type: text/plain 
																// and attachment at one time
					{
						// Check for UUEncoded message
						if ( line.startsWith( "begin " ) )
						{
							QByteArray filename;
							if ( isUUencoded( line, filename ) )
							{
								attachments_.push_back( 
									AttachmentItem( QString::fromUtf8( filename ), curLinePos ) );
								QByteArray temp;
								uudecode( false, temp );
							}
						}
						else
						{
							extractedText.append( line.buffer() );
							if ( !line.endsWith( '\n' ) && !extractedText.endsWith( "\r\n\r\n" ) )
							{
								extractedText.append( "\r\n" );
							}
						}
					}
				}
			} // Parsing of body
 		} // while
	}
	else if ( e_text == enumConType_ )
	{
		while ( !readline_.atEnd() )
		{
			line.clear();
			int curLinePos = readline_.pos();
			lineSize = readline_.readLine( line.buffer(), MaxLineSize ); 
			line.setSize( lineSize );

			// Check for UUEncoded message
			if ( line.startsWith( "begin " ) )
			{
				QByteArray filename;
				if ( isUUencoded( line, filename ) )
				{
					attachments_.push_back( 
						AttachmentItem( QString::fromUtf8( filename ), curLinePos ) );
					QByteArray temp;
					uudecode( false, temp );
				}
			}
			else
			{
				extractedText.append( line.buffer() );
				if ( !line.endsWith( '\n' ) && !extractedText.endsWith( "\r\n\r\n" ) )
				{
					extractedText.append( "\r\n" );
				}
			}
		}
	}

	encode( extractedText, encoding );

	QTextCodec *codec = QTextCodec::codecForName( charset.data() );

	if ( codec )
	{
		text_ = codec->toUnicode( extractedText );
	}
	else
	{
		codec = QTextCodec::codecForLocale();
		if ( codec )
		{
			text_ = codec->toUnicode( extractedText );
		}
		else
		{
			text_ = QString::fromLocal8Bit( extractedText );
		}
	}

	return Result;
}

//	============================================================
//	EmailMessage::decodeNonAsciiText
///
/// Decode message header parameter text that has rfc1522 
/// ( non Ascii ) format

QString EmailMessage::decodeNonAsciiText( JBufferString &text )
{
	int nStart = 0, nPoser = 0;
	QByteArray strResult;
	char retCharsetBuf[ MaxSize + 1 ];
	JBufferString retCharset( retCharsetBuf, MaxSize );
	char tempBuf[ MaxSize + 1 ];
	JBufferString temp( tempBuf, MaxSize );
	char temp1Buf[ MaxSize + 1 ];
	JBufferString temp1( temp1Buf, MaxSize );

	char encodBuf[ 128 ];
	JBufferString strEncoding( encodBuf, 128 );
	bool bShortForm = false;

	while ( -1 != ( nStart = text.indexOf( "=?" ) ) )
	{
		bShortForm = true;
		QByteArray strTemp;

		// Getting parts of encoded data

		// If string has non coded pieces, add them to result string
		if ( nStart != 0 )
		{
			temp.assign( text );
			temp.mid( 0, nStart );
			strResult += temp.data();
		}

		text.remove( 0, nStart + 2 );

		if ( -1 == ( nPoser = text.indexOf( '?' ) ) ) return "";

		retCharset.assign( text );
		retCharset.mid( 0, nPoser );

		text.remove( 0, nPoser + 1 );

		if ( -1 == ( nPoser = text.indexOf( '?' ) ) ||  nPoser != 1 ) return "";

		// Check encoding
		if ( 'Q' == text.at( 0 ) || 'q' == text.at( 0 ) )
		{
			strEncoding.assign( "quoted-printable", sizeof( "quoted-printable" ) - 1 );
		}
		else if ( 'B' == text.at( 0 ) || 'b' == text.at( 0 ) )
		{
			strEncoding.assign( "base64", sizeof( "base64" ) - 1 );
		}
		else 
		{
			text.remove( 0, 2 );

			temp1.assign( strResult.data(), strResult.size() );
			temp1.add( text );
			text.assign( temp1 );

			return "";
		}

		text.remove( 0, 2 );

		if ( -1 != ( nPoser = text.indexOf( "?=" ) ) )
		{
			temp1.assign( text );
			temp1.mid( 0, nPoser );
			strTemp = temp1.data();
			text.remove( 0, nPoser + 2 );
		}
		else return "";

		encode( strTemp, strEncoding, bShortForm );

		strResult += strTemp;
	}

	if ( !text.isEmpty() ) strResult += text.data();

	// Final Result
	text.assign( strResult.data(), strResult.size() );

    QString finalString;
	QTextCodec *codec = QTextCodec::codecForName( retCharset.data() );

	if ( codec )
	{
		finalString = codec->toUnicode( text.data() );
	}
	else
	{
		codec = QTextCodec::codecForLocale();
		if ( codec )
		{
			finalString = codec->toUnicode( text.data() );
		}
		else
		{
			finalString = QString::fromLocal8Bit( text.data() );
		}
	}

	return finalString;
}

//	============================================================
//	EmailMessage::encode
///
/// Decode the text if encoding is quoted-printable or Base64

int EmailMessage::encode( QByteArray &text, const JBufferString &encoding, bool bShortForm )
{
	int result = 0;
	QByteArray strLine;

	if ( encoding.compareNoCase( "quoted-printable" ) )
	{
		// Quoted-printable encoding
		int nPos;

		while ( -1 != ( nPos = text.indexOf( "=\r\n" ) ) )
		{
			text.remove( nPos, 3 );
		}

		// Replace substrings
		char nCode;
		//unsigned int i = 0;
		int i = 0;

		while ( i < text.length() )
		{
			if ( '=' == text[ i ] )
			{
				if ( isHexString( text.mid( i + 1, 2 ) ) )
				{
					// Replace some symbol
					nCode = HexCode( text[ i + 1 ] )*16 + HexCode( text[ i + 2 ] );
					text.remove( i, 3 );
					char c[ 2 ] = { nCode, '\0' };
					text.insert( i, ( char* ) c );
				}
				else
				{
					text.remove( i, 1 );
					if ( i > 0 ) i--;
				}
			}
			else if ( bShortForm && '_' == text[ i ] )
			{
				text[ i ] = ' ';
			}

			i++;
		}
	}
	else if ( encoding.compareNoCase( "base64" ) )
	{
		// Base64 encoding
		text = QByteArray::fromBase64( text );
	}
	else
	{
		result = 1;
	}

	return result;
}

//	============================================================
//	EmailMessage::isHexString

inline bool EmailMessage::isHexString( const QByteArray &hex )
{
	for ( int i = 0; i < hex.length(); i++ )
	{
		if ( !(( hex[ i ] >= '0' && hex[ i ] <= '9' ) ||
			( hex[ i ] >= 'a' && hex[ i ] <= 'f' ) ||
			( hex[ i ] >= 'A' && hex[ i ] <= 'F' )) )
		{
			return false;
		}
	}

	return true;
}

//	============================================================
//	EmailMessage::parseAddrList

void EmailMessage::parseAddrList( QStringList &addrList, JBufferString &paramText )
{
	StringTokenizer tokenizer( paramText.data(), "," );

	int lMark, rMark;
	char tempBuf[ MaxSize + 1 ];
	JBufferString temp( tempBuf, MaxSize );

	while ( tokenizer.nextToken( temp ) )
	{
		lMark = temp.indexOf( '<' );
		rMark = temp.indexOf( '>' );

		if ( -1 != lMark && -1 != lMark && rMark > lMark )
		{
			lMark += 1;

			QByteArray email = temp.data();
			email = email.mid( lMark, rMark - lMark );

			temp.remove( lMark - 1, temp.size() - lMark + 1 );
			temp.trim();

			if ( !temp.isEmpty() )
			{
				// If name enclose in '"'
				if ( temp.startsWith( '"' ) && temp.endsWith( '"' ) )
				{
					temp.remove( 0, 1 );
					temp.remove( temp.size() - 1, 1 );
				}

				QString toName = decodeNonAsciiText( temp );
				addrList.append( toName + " <" + email + ">" );
			}
			else
			{
				addrList.append( email );
			}
		}
		else
		{
			// If it has '@' then accept that as email otherwise accept as name
			if ( -1 != ( temp.indexOf( '@' ) ) )
			{
				addrList.append( QByteArray( temp.data() ) );
			}
			else
			{
				temp.trim();

				if ( temp.isEmpty() ) 
				{
					addrList.append( decodeNonAsciiText( temp ) );
				}
			}
		}
	}
}

//	============================================================
//	EmailMessage::getExt

void EmailMessage::getExt( const QByteArray &contentType, QByteArray &extension )
{
	int pos = contentType.indexOf( ';' );
	if ( -1 == pos ) pos = contentType.size();
	QByteArray subType = contentType.left( pos );

	// Default ext
	extension = "txt";

	// Get appropriate ext
	if ( subType.startsWith( "text/" ) )
	{
		if ( subType == constTextPlain )
		{
			extension = "txt";
		}
		else
		{
			extension = subType.right( subType.length() - sizeof( "text/" ) + 1 );
		}
	}
	else if ( compareNoCase( subType.data(), constRfc822 ) )
	{
		extension = "msg";
	}
	else if ( subType.startsWith( "image/" ) )
	{
		extension = subType.right( subType.length() - sizeof( "image/" ) + 1 );
	}
}

//	============================================================
//	EmailMessage::parseContentType

void EmailMessage::parseContentType( JBufferString &paramText )
{
	bool bFirst = true;

	StringTokenizer tokenizer( paramText.data(), ";" );

	int nMark = 0;

	char nameBuf[ MaxSize + 1 ];
	char tempBuf[ MaxSize + 1 ];
	JBufferString name( nameBuf, MaxSize );
	JBufferString temp( tempBuf, MaxSize );

	while ( tokenizer.nextToken( temp ) )
	{
		// First token determine type of parameter Content-Type
		// and for this is using enumerator enumConType_

		if ( bFirst )
		{
			// Parsing first subparameter : type / subtype
			contentType_ = temp.data();

			if ( -1 != ( nMark = temp.indexOf( '/' ) ) )
			{
				// Expect an error of several Content-Types in main Header
				name.assign( temp );
				name.mid( 0, nMark );

				// Determining the type of Content-Type
				if ( name.compareNoCase( constMultipart ) )
				{
					enumConType_ = e_multipart;
				} 
				else if ( temp.compareNoCase( constTextPlain ) || 
					temp.compareNoCase( constTextHtml ) || 
					temp.compareNoCase( constRfc822 ) )
				{
					enumConType_ = e_text;
				}
				else
				{
					enumConType_ = e_other;
				}
			}
			else
			{
				// Bad format of Content-Type and as default 
				// accept text/plain
				enumConType_ = e_text;
			}

			bFirst = false;
		}
		else
		{
			// Parsing other subparameters
			if ( -1 != ( nMark = temp.indexOf( '=' ) ) )
			{
				name.assign( temp );
				name.mid( 0, nMark );
				name.trim();

				temp.remove( 0, nMark + 1 );
				temp.trim();

				// Deleting enclosed quotation marks
				if (  temp.startsWith( '"' ) && temp.endsWith( '"' ) )
				{
					temp.remove( 0, 1 );
					temp.remove( temp.size() - 1, 1 );
				}

				if ( e_multipart == enumConType_ )
				{
					if ( name.compareNoCase( constBoundary ) )
					{
						boundary_ = QByteArray( "--" ) + QByteArray( temp.data() );
					}
				}

				if ( name.compareNoCase( constCharset ) )
				{
					charset_ = temp.data();
				}
			} // if
		} // else
	} // while
}

//	============================================================
//	EmailMessage::parseFrom

void EmailMessage::parseFrom( JBufferString &paramText )
{
	int lMark, rMark;
	char nameBuf[ MaxSize + 1 ];
	JBufferString name( nameBuf, MaxSize );

	lMark = paramText.indexOf( '<' );
	rMark = paramText.indexOf( '>' );

	if ( -1 != lMark && -1 != lMark && rMark > lMark )
	{
		lMark += 1;

		name.assign( paramText );
		name.mid( lMark, rMark - lMark );

		paramText.remove( lMark - 1, paramText.size() - lMark + 1 );
		paramText.trim();

		if ( !paramText.isEmpty() )
		{
			// If name enclosed in '"'
			if ( paramText.startsWith( '"' ) && paramText.endsWith( '"' ) )
			{
				paramText.remove( 0, 1 );
				paramText.remove( paramText.size() - 1, 1 );
			}
		}

		sender_ = decodeNonAsciiText( paramText );
		sender_ += QByteArray( " <" ) + QByteArray( name.data() ) + ">";
	}
	else
	{
		sender_ = paramText.data();
	}
}

//	============================================================
//	EmailMessage::parseDate

void EmailMessage::parseDate( JBufferString &paramText )
{
	// Parse date which has format like following:
	// Tue, 1 Aug 2006 10:30:34 +0300

	// Delete weekday
	int comma = 0;
	if ( ( comma = paramText.indexOf( ',' ) ) != -1 )
	{
		paramText.remove( 0, comma + 1 );
	}

	// Delete timezone as all after time
	int pos = paramText.lastIndexOf( ':' );
	pos = paramText.indexOf( ' ', pos );

	if ( -1 != pos )
	{
		paramText.remove( pos, paramText.size() - pos );
	}

	paramText.trim();
	QByteArray dateText( paramText.data(), paramText.size() );

	QDateTime date = QDateTime::fromString( dateText, "d MMM yyyy hh:mm:ss" );
	time_t emailDate = date.toTime_t();
	-1 != emailDate ? date_ = emailDate : date_ = 0;
}

//	============================================================
//	EmailMessage::parseContentDisp

void EmailMessage::parseContentDisp( JBufferString &paramText, bool &bMulti, 
	bool bTextEmpty, int boundaryPos, int &nameCounter )
{
	bool bFirst = true;
	bMulti = false;
	attachName_ = "";
	attachPos_ = 0;
	bAddAttachment_ = false;

	StringTokenizer tokenizer( paramText.data(), ";" );

	char subBuf[ MaxSize + 1 ];
	JBufferString sub( subBuf, MaxSize );
	char nameBuf[ MaxSize + 1 ];
	JBufferString name( nameBuf, MaxSize );

	int nMark = 0;

	while ( tokenizer.nextToken( name ) )
	{
		name.trim();

		if ( bFirst )
		{
			// An attachment if
			if ( name.compareNoCase( "attachment" ) )
			{
				bMulti = true;
			}
			else if ( name.compareNoCase( "inline" ) )
			{
				bMulti = true;
			}

			bFirst = false;
		}
		else if ( bMulti )
		{
			if ( -1 != ( nMark = name.indexOf( '=' ) ) )
			{
				sub.assign( name );
				sub.mid( 0, nMark );
				sub.trim();

				name.remove( 0, nMark + 1 );
				name.trim();

				// Deleting enclosed quotation marks
				if ( name.startsWith( '"' ) && name.endsWith( '"' ) )
				{
					name.remove( 0, 1 );
					name.remove( name.size() - 1, 1 );
				}

				if ( sub.compareNoCase( "filename" ) )
				{
					// Name of attachment
					attachName_ = decodeNonAsciiText( name );
					bAddAttachment_ = true;
					attachPos_ = boundaryPos;
				}
			}
		}
	} // while tokenizer has more tokens

	if ( bMulti && !bAddAttachment_ )
	{
		if ( e_text == enumConType_ && bTextEmpty )
		{
			bMulti = false;
		}
		else
		{
			QByteArray extension;
			getExt( contentType_, extension );

			// Check for empty attachment name
			nameCounter++;
			attachName_ = QString::number( nameCounter ) + "." + extension;
			bAddAttachment_ = true;
			attachPos_ = boundaryPos;
		}
	}
}

//	============================================================
//	EmailMessage::compareNoCase

bool EmailMessage::compareNoCase( const char *ca1, const char *ca2 )
{
	long len = strlen( ca1 );
	if ( len != strlen( ca2 ) ) return false;

	while ( --len >= 0 )
	{
		if ( tolower( *( ca1 + len ) ) != tolower( *( ca2 + len ) ) )
		{
			return false;
		}
	}

	return true;
}

//	============================================================
//	EmailMessage::addAttachment

void EmailMessage::addAttachment()
{
	if ( bAddAttachment_ )
	{
		attachments_.push_back( AttachmentItem( attachName_, attachPos_ ) );
		bAddAttachment_ = false;
	}
}

//	============================================================
//	EmailMessage::parseHeaderOnly

void EmailMessage::parseHeaderOnly( bool bOnly )
{
	bParseHeaderOnly_ = bOnly;
}

//	============================================================
//	EmailMessage::isUUencoded

bool EmailMessage::isUUencoded( JBufferString &line, QByteArray &filename )
{
	QTextCodec *codec = QTextCodec::codecForLocale();
	QString uline;

	if ( codec )
	{
		uline = codec->toUnicode( line.data(), line.size() );
	}
	else
	{
		uline = QString::fromUtf8( line.data(), line.size() );
	}

	QStringList tokens = uline.split( ' ', QString::SkipEmptyParts );

	if ( tokens.size() != 3 )
	{
		return false;
	}

	for ( int i = 0; i < tokens.at( 1 ).size(); i++ )
	{
		if ( !tokens.at( 1 ).at( i ).isDigit() )
		{
			return false;
		}
	}

	filename = tokens.at( 2 ).trimmed().toUtf8();
	return true;
}

//	============================================================
//	EmailMessage::uudecode

#define DEC(c)  (((c) - ' ') & 077)
void EmailMessage::uudecode( bool extract, QByteArray &outData )
{
	if ( extract )
	{
		outData.clear();
	}

	const int bufLen = 4096;
	char buf[ bufLen + 1 ];
	JBufferString bufstr( buf, bufLen );

	register int n = 0;
	register char ch = 0, *p = 0;

	while ( !readline_.atEnd() )
	{
		bufstr.clear();
		int read = readline_.readLine( bufstr.buffer(), bufLen );
		bufstr.setSize( read );

		if ( bufstr.startsWith( "end" ) )
		{
			break;
		}

		if ( extract )
		{
			p = buf;
			n = DEC (*p);

			if (n <= 0)
			break;

			for ( ++p; n > 0; p += 4, n -= 3 )
			{
				if ( n >= 3 )
				{
					ch = DEC (p[0]) << 2 | DEC (p[1]) >> 4; 
					outData.append( ch );
					ch = DEC (p[1]) << 4 | DEC (p[2]) >> 2; 
					outData.append( ch );
					ch = DEC (p[2]) << 6 | DEC (p[3]);
					outData.append( ch );
				}
				else
				{
					if (n >= 1)
					{
						ch = DEC (p[0]) << 2 | DEC (p[1]) >> 4; 
						outData.append( ch );
					}
					if (n >= 2)
					{
						ch = DEC (p[1]) << 4 | DEC (p[2]) >> 2; 
						outData.append( ch );
					}
				}
			}
		}
	}
}

